React TypeScript -- React 使用 TypeScript 开发
React TypeScript
1. 概述
1.1 创建应用
创建支持 TypeScript 语法的 React 应用
npx create-react-app <appname> --template typescript
1.2 文件后缀
如果文件中包含 React 组件或者 JSX 代码,文件后缀使用 tsx
如果文件中不包含任何 JSX 代码,文件后缀使用 ts
2. 为组件添加类型
在我们定义了组件以后,TypeScript 编译器并不知道我们定义的是组件,它会认为我们定义的就是一个普通的函数。
在类型认知出现偏差以后,TypeScript 编译器不能正确的对我们的代码进行约束。
比如在下列代码中,我们通过组件获取组件下的属性,TypeScript 编译器会报错,说组件下不存在这个属性。
当 TypeScript 编译器知道我们定义的是组件以后,当我们错误的使用了组件以后,它才能准确的为我们进行提示。
const Child= () => {
return <div>Child</div>;
};
// 类型 "() => Element" 上不存在属性 "displayName"。
console.log(Child.displayName);
// 类型 "() => Element" 上不存在属性 "defaultProps"。
console.log(Child.defaultProps);
2
3
4
5
6
7
8
import { FC } from "react";
const Child: FC = () => {
return <div>Child</div>;
};
2
3
4
5
3. Props
为组件 props 定义接口类型,编译器可以检查父组件在调用该组件时是否正确的传递了 props,在子组件内部是否正确的使用了 props。
// src/props/Child.tsx
interface Props {
color: string;
onClick: () => void;
}
const Child: FC<Props> = ({ color, onClick }) => {
return <div onClick={onClick}>{color}</div>;
};
2
3
4
5
6
7
8
9
// src/props/Parent.tsx
const Parent = () => {
return <Child color="red" onClick={() => console.log("clicked")} />;
};
2
3
4
4. state
// src/state/Guests.tsx
import { useState, FC } from "react";
const Guests: FC = () => {
const [name, setName] = useState<string>("");
// 此处如果不为 guests 指定类型, 类型将会是 never[]
const [guests, setGuests] = useState<string[]>([]);
const clickHandler = () => {
setName("");
// 如果 guests 是 never[], 那么字符串 name 将不能被存储到 guests 数组中
setGuests([...guests, name]);
};
return (
<>
<ul> {guests.map((guest) => <li key={guest}>{guest}</li>)}</ul>
<input type="text" value={name} onChange={(event) => setName(event.target.value)}/>
<button onClick={clickHandler}>add</button>
</>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/state/UserSearch.tsx
import { useState, FC } from "react";
const users = [
{ name: "张三", age: 20 },
{ name: "李四", age: 30 },
];
const UserSearch: FC = () => {
const [name, setName] = useState<string>("");
// 在组件初次渲染, 在没有找到 user 的情况下, user 的类型是 undefined
// 在找到 user 以后, 它的类型是 {name: string, age: number}
// 所以 user 的类型就应该是 {name: string, age: number} | undefined
const [user, setUser] = useState<{ name: string; age: number } | undefined>();
// 搜索用户
const searchHandler = () => {
// find 方法的返回值可能是 user, 也可能是 undefined
setUser(
users.find((user) => user.name === name)
);
};
return (
<>
<input type="text" value={name} onChange={(event) => setName(event.target.value)} />
<button onClick={searchHandler}>search</button>
{user && JSON.stringify(user)}
</>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
5. 事件对象
// src/event/EventComponent.tsx
import { ChangeEvent, FC, DragEvent } from "react";
const EventComponent: FC = () => {
// 参数"event"隐式具有"any"类型
const changeHandler = (event: ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};
const dragStartHandler = (event: DragEvent<HTMLDivElement>) => {
// event.target: 返回触发事件的元素
// event.currentTarget: 返回绑定事件的元素
console.log(event.target);
console.log(event.currentTarget);
};
return (
<>
<input type="text" onChange={changeHandler} />
<div draggable onDragStart={dragStartHandler}> drag event </div>
</>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
6. ref
src/ref/RefComponent.tsx
import { FC, useRef, useEffect } from "React";
const RefComponent: FC = () => {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (!inputRef.current) return;
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
};
2
3
4
5
6
7
8
9
10
7. Redux
npm install redux redux-thunk axios react-redux @types/react-redux --save-exact
# save-exact: 在 package.json 文件中记录安装包的确切版本
2
redux、redux-thunk、axios 内置 TypeScript 类型声明文件,所以不需要单独下载。
react-redux 没有内置类型声明文件,所以需要单独下载。
需求:向 npm 发送请求加载 npm 包列表信息。
第一步:定义 Action Type
// src/state/action-types/package.action.types.ts
export enum searchActionType {
// 请求中
SEARCH_PACKAGES = "search_packages",
// 请求成功
SEARCH_PACKAGES_SUCCESS = "search_packages_success",
// 请求失败
SEARCH_PACKAGES_ERROR = "search_packages_error",
}
2
3
4
5
6
7
8
9
// src/state/action-types/index.ts
export * from "./package.action.types";
2
第二步:定义 Action 对象类型、Reducer 函数的 action 参数 action 类型
// src/state/actions/packages.action.ts
import { searchActionType } from "../action-types";
/**
* 请求: {type: "search_packages"}
* 成功: {type: "search_packages_success", payload: ["react", "react-dom"]}
* 失败: {type: "search_packages_error", error: "Request Failed"}
*/
interface SearchPackagesAction {
type: searchActionType.SEARCH_PACKAGES;
}
interface SearchPackagesSuccessAction {
type: searchActionType.SEARCH_PACKAGES_SUCCESS;
payload: string[];
}
interface SearchPackagesErrorAction {
type: searchActionType.SEARCH_PACKAGES_ERROR;
error: string;
}
export type SearchAction =
| SearchPackagesAction
| SearchPackagesSuccessAction
| SearchPackagesErrorAction;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/state/actions/index.ts
export * from "./packages.action";
2
第三步:创建 Reducer 函数,匹配 Action Type 返回对应的状态
// src/state/reducers/packages.reducer.ts
import { searchActionType } from "../action-types";
import { SearchAction } from "../actions";
export interface PackagesState {
loading: boolean;
error: string | null;
list: string[];
}
const initialState: PackagesState = {
loading: false,
error: null,
list: [],
};
export default function packagesReducer(
state: PackagesState = initialState,
action: SearchAction
): PackagesState {
switch (action.type) {
case searchActionType.SEARCH_PACKAGES:
return { loading: true, error: null, list: [] };
case searchActionType.SEARCH_PACKAGES_SUCCESS:
return { loading: false, error: null, list: action.payload };
case searchActionType.SEARCH_PACKAGES_ERROR:
return { loading: false, error: action.error, list: [] };
default:
return state;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
第四步:合并 reducer 函数
// src/state/reducers/index.ts
import { combineReducers } from "redux";
import packagesReducer from "./packages.reducer";
export const reducers = combineReducers({
packages: packagesReducer,
});
2
3
4
5
6
7
第五步:创建用于发送请求获取 npm 包的 action creator 函数
// src/state/action-creators/packages.action.creators.ts
import axios from "axios";
import { Dispatch } from "react";
import { searchActionType } from "../action-types";
import { SearchAction } from "../actions";
export const searchPackages =
(key: string) => async (dispatch: Dispatch<SearchAction>) => {
dispatch({
type: searchActionType.SEARCH_PACKAGES,
});
try {
const { data } = await axios.get(
`https://registry.npmjs.org/-/v1/search`,
{
params: {
text: key,
},
}
);
dispatch({
type: searchActionType.SEARCH_PACKAGES_SUCCESS,
payload: data.objects.map((item: any) => item.package.name),
});
} catch (error) {
if (error instanceof Error) {
dispatch({
type: searchActionType.SEARCH_PACKAGES_ERROR,
error: error.message,
});
}
}
};
// unknow 是更加严格的 any 类型.
// 在对 unknown 类型的值执行大多数操作之前, 我们必须进行某种形式的检查
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export * from "./packages.action.creators";
第五步:创建 Store 对象,配置 redux-thunk 中间件函数
import { applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk";
import { reducers } from ".";
export const store = createStore(reducers, applyMiddleware(thunk));
2
3
4
5
第六步:创建 state 入口文件
// src/state/index.ts
export * as actionCreators from "./action-creators";
export * from "./reducers";
export * from "./store";
2
3
4
第七步:配置 Provider 组件
// src/components/App.tsx
import { FC } from "react";
import { Provider } from "react-redux";
import { store } from "../state";
import Packages from "./Packages";
export const App: FC = () => (
<Provider store={store}>
<Packages />
</Provider>
);
2
3
4
5
6
7
8
9
10
11
第八步:在组件中,当点击按钮时向服务器端发送请求获取 npm 包
// src/components/Packages.tsx
import { FC, FormEvent, useState } from "react";
import { useActions } from "../hooks/useActions";
const Packages: FC = () => {
const [key, setKey] = useState<string>("");
const { searchPackages } = useActions();
const onSubmitHandler = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
searchPackages(key);
};
return (
<form onSubmit={onSubmitHandler}>
<input type="text" value={key} onChange={(event) => setKey(event.target.value)} />
<button>search</button>
</form>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/hooks/useActions.ts
import { useDispatch } from "react-redux";
import { bindActionCreators } from "redux";
import { actionCreators } from "../state";
export const useActions = () => {
const dispatch = useDispatch();
return bindActionCreators(actionCreators, dispatch);
};
2
3
4
5
6
7
8
9
第九步:在组件中获取状态并根据状态渲染 UI
import { useSelector } from "react-redux";
import { AppState } from "../state";
import { PackagesState } from "../state/reducers/packages.reducer";
const Packages: FC = () => {
const state = useSelector<AppState, PackagesState>((state) => state.packages);
return (
<>
{state.loading && <div>loading....</div>}
{state.error && <div>{state.error}</div>}
{state.list.map((name) => (
<div key={name}>{name}</div>
))}
</>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
定义应用全局状态的类型,用于传递给 useSelecter 钩子函数
// src/state/reducers/index.ts
export type AppState = ReturnType<typeof reducers>;
2
第十步:优化为应用全局状态设置类型的代码
// src/hooks/useTypedSelector.ts
import { useSelector, TypedUseSelectorHook } from "react-redux";
import { AppState } from "../state/reducers";
export const useTypedSelector: TypedUseSelectorHook<AppState> = useSelector;
2
3
4
5
// src/components/Packages.tsx
import { useTypedSelector } from "../hooks/useTypedSelector";
const Packages: FC = () => {
const state = useTypedSelector((state) => state.packages);
};
2
3
4
5
6